package top.v5it.japi.plus.support.lock;

import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import top.v5it.japi.plus.support.LuaScriptUtil;

import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基于redis实现分布式锁
 *
 * @author zhanpu
 * @date 20-11-30
 */
@Slf4j
public class RedisShardLock {

    /**
     * key
     */
    private final String key;

    /**
     * value
     */
    private final String value;

    /**
     * RedisTemplate
     */
    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * store thread
     */
    private final ConcurrentMap<Thread, LockData> THREAD_MAP;

    /**
     * 构造
     *
     * @param key
     * @param value
     * @param redisTemplate
     */
    public RedisShardLock(String key, String value, RedisTemplate<String, Object> redisTemplate) {
        this.key = key;
        this.value = value;
        this.redisTemplate = redisTemplate;
        this.THREAD_MAP = new ConcurrentHashMap<>();
    }

    /**
     * lock
     *
     * @param timeout 单位 MILLISECONDS
     */
    public boolean lock(long timeout) {
        Thread currentThread = Thread.currentThread();
        LockData lockData = THREAD_MAP.get(currentThread);
        if (Objects.nonNull(lockData)) {
            lockData.recount.incrementAndGet();
            log.debug("+++++[{}]重入加锁成功+++++", currentThread);
            return true;
        }

        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);
        if (Objects.nonNull(result) && Boolean.TRUE.equals(result)) {
            LockRenewal lockRenewal = new LockRenewal(key, value, TimeUnit.SECONDS.convert(timeout, TimeUnit.MILLISECONDS), redisTemplate);
            long l = ThreadLocalRandom.current().nextLong();
            Thread thread = ThreadUtil.newThread(lockRenewal, "lock-thread-" + l, true);
            thread.start();

            lockData = new LockData(thread, lockRenewal);
            THREAD_MAP.put(currentThread, lockData);

            log.debug("+++++[{}]加锁成功+++++", currentThread);

            return true;

        } else {
            log.debug("+++++[{}]加锁失败+++++", currentThread);

            return false;
        }
    }

    /**
     * unlock
     */
    public void unlock() {
        Thread currentThread = Thread.currentThread();
        LockData lockData = THREAD_MAP.get(currentThread);
        if (Objects.isNull(lockData)) {
            throw new IllegalMonitorStateException();
        }

        AtomicInteger recount = lockData.recount;
        if (recount.get() < 0) {
            throw new IllegalMonitorStateException();
        } else if (recount.get() == 0) {

            try {
                lockData.lockRenewal.stop();
                lockData.thread.interrupt();
                Long result = redisTemplate.execute(LuaScriptUtil.getLuaUnlockScript(), Collections.singletonList(key), value);
                if (Objects.nonNull(result) && result.compareTo(0L) == 0) {
                    log.debug("+++++[{}]锁不存在+++++", currentThread);
                }
            } finally {
                THREAD_MAP.remove(currentThread);
            }

            log.debug("+++++[{}]锁释放成功+++++", currentThread);
        } else {
            lockData.recount.decrementAndGet();
            log.debug("+++++[{}]重入锁释放成功+++++", currentThread);
        }
    }

    /**
     * 重试3次获取锁
     *
     * @param timeout
     * @return
     * @throws InterruptedException
     */
    public boolean tryLock(long timeout) throws InterruptedException {
        int reTry = 3;
        while (reTry > 0) {
            if (lock(timeout)) {
                return true;
            }
            TimeUnit.MILLISECONDS.sleep(300L);
            reTry--;
        }
        return false;
    }

    static final class LockData {

        final AtomicInteger recount;
        final transient Thread thread;
        final LockRenewal lockRenewal;

        LockData(Thread thread, LockRenewal lockRenewal) {
            this.thread = thread;
            this.lockRenewal = lockRenewal;
            this.recount = new AtomicInteger(0);
        }
    }


}
